iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
AI Engineering

AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄系列 第 27

Day 27|把「不做」變成結構:我刻意沒有做的那些

  • 分享至 

  • xImage
  •  

我沒有做的那些:工具牆上四個空鉤子,分別吊著自己餵自己、程式化送入、收你的 git 金鑰、完整帳號生命週期,另有一格問號還在評估

簡短回顧

昨天講的是終端本身:我為了加入 --auth-url 旗標,改寫了 ttyd我的 fork 版);也講了在沒有明文規格的情況下,怎麼用特徵測試確認重寫沒有破壞既有行為。

今天講這套東西的邊界:哪些能力沒有被做成預設的產品表面,以及為什麼。這一篇講的多半是「沒有」,而沒有這種事光靠作者說沒有是不算數的,所以路徑先給:資料模型在 claude-pty/server/models.py,控制平面的全部路由在 claude-pty/server/app.py

不做的每一條,對審查員的意思都一樣:少一個需要信任的表面。不收 SSH 私鑰,它就不必保管一把可能通往多台主機的鑰匙;repo 的 clone/push 改由使用者自己的 GitLab PAT 經代理完成,實際能做什麼仍由該 token 與 GitLab 上的帳號權限決定。控制平面不另外提供程式化送入 API,使用者不會在產品介面裡看到一條可以直接餵 prompt 的路;帳號管理則只做到這一輪需要的基本面:管理員能建立帳號,也能查看與操作所有人的 Session,但沒有完整的帳號生命週期。

沒有做的東西,要怎麼講才有意義?

前面幾天講的都是做出來的東西:容器怎麼起、身分怎麼驗、畫面怎麼接回來。這些都有成品,截圖貼上去就有人看得懂。

沒做的東西不一樣。它沒有截圖,只有一份「這裡本來可以有什麼」的清單,而清單這種東西,光憑作者說「我想過」是撐不起來的。

所以我給自己訂了一個規矩:每一條「沒做」,都要拿得出「這條路是通的」的具體證據。不是靠我說我做得出來,是靠外面就有人在走這條路、或者評估報告白紙黑字寫在那裡。做得到,不需要靠自己做過來證明。

但這篇不是要列一張「我答應不做什麼」的清單,而是要分清楚:哪些承諾仍靠人記得,哪些已經從路由、資料模型與預設流程中被拿掉。

一共四條,外加一條還在評估的。

讓 AI 自己餵 AI,這條路真的通嗎?

通。而且不用自己發明,市面上已經有做得比我完整的東西。

herdr 是一套多 agent 的協作介面,它把「操控一個正在跑的 CLI」做成了公開的 socket API。它的官方文件大致提到這些能力:

  • pane.send_textpane.send_keyspane.send_input:往某一個面板送文字、送按鍵、送輸入
  • agent.prompt:直接給某個 agent 一句 prompt,而且可以附一個 wait 物件
  • agent.wait:等這個 agent 進入某個狀態才回來,支援等到它 blocked、也支援等到它 done
  • events.subscribe:訂閱事件,例如 pane.agent_status_changed

最後那兩個是關鍵。有了 agent.wait 跟狀態事件,你可以寫出這樣的東西:A 跑到卡住了,程式偵測到 blocked,自動把下一句餵進去;或者 A 做完了,把它的結果轉成 B 的 prompt 送過去。整條流水線不需要人在旁邊。

這不是漏洞,也不是誰在鑽什麼縫。它是人家設計出來、寫進文件、公開給大家用的功能。我提它只有一個用意:證明這條路是通的、而且已經有人走在上面

我要說的只是:同樣的形狀,我這套也組得出來,但沒有把它做成控制平面的能力。

那如果我真的接上去,會怎麼樣?

假設我在啟動之後,讓程式自己去 send keys、自己餵一句 prompt 進去。技術上不難,上面那些就是現成的形狀;而且目前 ttyd 終端本來就是可寫的,有存取權的人若另外實作 WebSocket 協定或操作瀏覽器,仍然可以自動輸入。我沒有做的不是這個底層能力,而是沒有替它再開一條專用、容易直接呼叫的控制面。

我沒有往那邊走,是因為讀完官方的使用條款之後,覺得這裡有風險。

Anthropic 的 Consumer Terms of Service(2025 年 10 月 8 日生效)第 3 節第 7 款,列的是「你不得從事的行為」,其中一條逐字是這樣:

Except when you are accessing our Services via an Anthropic API Key or where we otherwise explicitly permit it, to access the Services through automated or non-human means, whether through a bot, script, or otherwise.

翻成中文:除非你是透過 Anthropic API Key 存取本服務,或者我們另行明示允許,否則不得以自動化或非人為的方式存取本服務,無論是透過機器人、腳本或其他方式。

這一條有兩個豁免:走 API Key,或官方明示允許。

我無法替這條款定案。什麼算「自動化」、什麼算「人為」、人按下按鈕後由程式代為動作算不算,法律上的邊界不是我能決定的。我能決定的是:這個功能沒有重要到值得讓系統靠近那片灰區。

有人問,那按下「開一場」算不算?控制平面確實會把模型、思考深度與 session id 組成指令列,再把 CLI 叫起來;但它只把互動畫面開在那裡等人輸入,沒有送出 prompt,也沒有替使用者發起模型任務。

所以我把自己的設計界線畫在下一步:由軟體產生或送出 prompt。這是我選擇站的位置,不是我宣稱的法律界線。

所以這套東西能講的那句話,必須收斂得很精確:

目前的控制平面不會替使用者產生或送出 prompt。

不是「軟體無法送字」,因為指令列上的參數確實是軟體組的,可寫終端也仍能被另外自動化。這句話描述的是目前產品提供的流程:它負責把 CLI 叫起來,停在那裡等人輸入,沒有在控制平面另外提供 /input/keys 之類的送入 API。

我特別在意這句話的精確度,是因為前面幾天我一直在清一種東西:註解裡寫著一個它其實沒有做到的效果。宣稱只要比機制大一點點,它就會讓下一個人放棄檢查。劃界線這件事也一樣,界線不能宣稱得比實際的機制多。

一句「我們不會這樣做」,值多少?

第二條沒做的東西,是第一條的延伸:這套東西的控制平面沒有專用的程式化送入 API。沒有 /input、沒有 /keys,也沒有一個「把這段文字送進那一場」的端點。這不等於底層終端無法被自動化,上一節那條路仍然在。這裡刻意省掉的,是產品替自動送入準備好的那條捷徑。

上一節那條線如果只寫在文件裡、端點卻還在,那它就只是一句承諾。而承諾不會自己維持:維護者換一個人,或者哪天手上有個很煩的重複工作,一個內部端點就加進去了,反正只有自己人在用。沒有任何機制會攔它。

所以這裡有一個問法,你可以拿去問任何一套工具,不只是我這套:它說「我們不會把 X 做成產品能力」的時候,控制平面有沒有替 X 留下一個可直接呼叫的開口? 有現成端點而承諾不呼叫,那句話的壽命等於現在維護它的那個人還記得多久;沒有那個端點,至少得先有人動手新增一個產品表面。這件事也查得出來:看的不是全文搜尋撈到幾筆,而是路由表上有沒有那條路徑、資料模型上有沒有那個欄位。這兩個地方窮舉得完,而全文搜尋會撈到註解、測試夾具,還有剛好拿同一個字命名的別的東西。

這個問法反過來也綁住我自己。我不能只說「我不會」,我得讓它不成為控制平面中預設、可直接呼叫的產品能力,而且讓這件事是你查得到的。

那 git 呢?我要不要收你的鑰匙?

第三條,而且這一條最難決定。

前面兩條都是「不要有那個能力」,講起來乾脆。這一條不一樣,因為它有一個實際需求擋在前面:容器裡的 AI 要能 git clone、要能 push。做不到的話,這整套東西的用途少掉一大半。

而 git 的認證,繞不開「你手上要有什麼」。

第一條路是最直接的:請你把 SSH 私鑰貼給我。 我沒有走,理由不需要想很久。那把鑰匙能以你的身分登入任何一台信任它的主機,不只是 git。你要撤銷的話,得自己跑去每一台改。收下它的那一刻,我就變成保管人,而這個責任我承受不起。

第二條路看起來聰明多了:SSH agent 轉發。 我不持有你的鑰匙,只是把 agent 的 socket 轉進容器,簽章的動作永遠發生在鑰匙那一側。

這條路 Day 19 就走過了:人自己跑容器的那支腳本,本來就會把 host 的 agent 帶進去。claude-pty 網頁開出的 Session 則預設不掛,一般使用者也不能替自己或單一 Session 開啟。要帶得由部署者設定 CLAUDE_PTY_SSH_AUTH_SOCK,而且一開就是所有 Session 共用同一個 socket(ADR 0011)。

所以 SSH agent 在這裡不是 GitLab 的預設路徑,而是一個部署層的全域 escape hatch。如果沒有另外設定 destination constraints,它可能被拿去認證其他連得到、也信任那把 key 的主機。這不是某位使用者能不能打開的選項,而是部署這台機器的人願不願意把這份能力延伸給所有 Session。

前面幾天做的 per-user 狀態隔離救不了這件事,因為那隔的是狀態,不是這條共用的通道。要限縮只能在 Host 那一側處理,例如使用獨立的 agent 與受限 key,或在加入 key 時設定 destination constraints;這套工具目前沒有替部署者完成這一層。

第三條路是現在走的:換一種憑證,然後改寫網址。

收的不是鑰匙,是一個 GitLab 的 personal access token。差別在於它可以設到期、可以在 GitLab 集中撤銷,而且實際能做什麼不會超過該帳號原本擁有的權限。

而它不會進入 Session 容器。中間隔著一顆 NGINX 反向代理,每個設了 token 的使用者一顆,由系統替他建立與維護;使用者知道代理存在,但不需要自己操作它。GitLab 的 HTTPS、git@host:ssh://git@host/ 三種網址都會由 insteadOf 改寫成走代理;即使部署者另外開了 SSH agent,這些 GitLab 網址仍走代理。容器裡的 git 照打不誤,Authorization 由代理補上;token 加密存放在資料庫,啟動代理時才寫入代理容器的 NGINX 設定。檢查 Session 容器的 env、labels、mounts 與 command,都找不到 token。

三條路排在一起,差別其實是同一個問題的三種答案:出事的時候,你要去找誰?

第一條要找我,因為東西在我手上。第二條要找部署這台機器的人,而且波及的是所有人。第三條你自己去 GitLab 按撤銷,不必經過我。

我要講清楚,這條路不是沒有缺口。目前不支援 git-lfs:代理只放行一般 Git smart HTTP,沒有放行 LFS 的 batch API。有用 LFS 的 repo 會在取得 LFS 物件時明確失敗;若另外設定跳過下載錯誤,工作目錄才可能只留下 pointer file。這一條我目前沒有解,只能先講明白。

非拿不可,只能縮小意外暴露

上一節整節在講不收你的鑰匙。但有一把我閃不掉:Claude Code CLI 自己的登入憑證。

沒有它,容器裡那個 AI 連開場都過不去,會停在登入提示。以我當時使用的 Claude Code 與這套實作,它不能像 GitLab token 一樣留在 Session 容器外、交由代理代為使用;因為它不是 agent 要去呼叫的某個服務,它就是 agent 自己的身份。

於是這裡有一個不對稱,而我一直沒有把它講出來:GitLab 的 token 為了不進 Session 容器,我蓋了一顆代理;CLI 自己的 token,卻是用環境變數直接送進 Session 容器的。

先講為什麼我認為那不是疏忽。兩種憑證的性質不同:

  • 借來的人類身份(GitLab 的 token):agent 只是拿它去呼叫一個服務。所以可以放在代理那一層,讓 Session 裡的 agent 碰不到 token 明文
  • agent 自己的身份(CLI 的憑證):它是那個 client 本人。在目前這條啟動路徑裡,CLI 必須取得自己的登入憑證;我沒有另外替它做一層憑證代理

但是分類講得通,不代表現在的做法已經夠好。

這件事是我自己先撞到的:我在 CLI 裡叫它印一次環境變數,找不到那個憑證變數。在我測試的版本中,能確認的是結果:Claude Code 呼叫 shell 工具時,沒有把憑證變數交給子程序。 它究竟是先移除變數,還是另建一份排除敏感欄位的環境,我沒有公開文件或原始碼可以定案。

這個結果也合理。那個 CLI 會開 shell,shell 會執行 AI 要求的任何指令;至少在我測到的 shell 工具邊界,它沒有把自己的憑證一起交出去。

而我做了什麼?我把那個憑證塞進容器的環境變數。也就是說:

  1. docker inspect 直接印得出來。 而這套系統裡,控制平面跟對帳程序都掛著 Docker 的 socket
  2. /proc/1/environ 讀得到
  3. CLI 啟動前由 entrypoint 叫起來的其他程序也可能繼承它。 舊版的 mitmweb 就在這個範圍裡;但 Claude Code 後來呼叫的 shell 子程序,在我的實測中反而看不到它

第一點不用想像,docker inspect 一下,CLAUDE_CODE_OAUTH_TOKEN 就躺在 Env 陣列裡:

docker inspect 一顆 Session 容器,Env 陣列裡 CLAUDE_CODE_OAUTH_TOKEN 就躺在那些平凡的設定值中間

這種洩漏最麻煩的地方就在這裡:環境變數這個容器不分貴賤。 你貼一段 docker inspect 的輸出去問人問題的時候,不會意識到自己順手貼了什麼。

第三點是外層容器自己多開的暴露面。

還有另一條路。CLI 能從 file descriptor 讀取 token;這一點我已經用實際啟動與 /status 驗證。公開線索則來自 Claude Code 官方 repository 裡的一張使用者 bug 回報(#57925):回報者在 Claude Code on Web 環境看到 CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR=4。這能佐證官方環境至少曾採用相同的形狀,但它不是官方文件承諾的公開介面。

順帶一提,那張 issue 描述的架構跟我這裡幾乎一樣:一個本機的反向代理(127.0.0.1)、一個固定的使用者名稱(local_proxy)、HTTP Basic 認證。

那條路解掉的是什麼,我想講精確一點。它不是把秘密藏起來,容器裡仍然有一個能以那個身分執行程式的東西,因為那個東西就是 AI 本身。它解掉的是值不再進環境區塊,也就是少掉幾個容易意外洩漏的表面:docker inspect、PID 1 的環境區塊,以及 CLI 啟動前由 entrypoint 叫起來的其他程序。環境傾印或除錯 log 也不再有那個值可印。憑證外流多半就是從這種地方走的,不是從有人精心攻進容器。

所以這條路我走了。做法是:token 預設不再放進 env(建立表單留了一個切回去的開關),改成在容器建立好、還沒啟動的那個縫隙裡,把它寫進容器自己的檔案系統(跟前面那顆代理拿到設定檔是同一招)。啟動腳本開成一個 fd,接著立刻把檔案刪掉,然後只把一個數字交給 CLI。

檔案刪掉之後 fd 仍然有效,這是 Unix 的老性質。容器裡 ls 找不到、docker inspect 印不出來、/proc/1/environ 只看得到一個 4

但「檔案找不到」不等於「內容拿不到」,這句我後來才量清楚:只要還有行程握著那個 fd,同一個容器裡同 uid 的程式就能從 /proc/<pid>/fd/4 原樣再讀一次,而 CLI 讀憑證的方式是另外開 /proc/self/fd/4,並不會消耗掉我們手上那個。所以後來改成把內容灌進一條 anonymous pipe(被讀一次就空了),並且把 fd 的建立推到啟動 CLI 前的最後一刻:fd 一開,之後 fork 的每個行程都繼承,實測那一版連 mitmweb 都握著它,而它跟憑證毫無關係。這換來的不是「AI 拿不到憑證」,它跟 CLI 同一個 uid、又能執行任意指令;換掉的是那條最省事的路。

過程中踩了一個坑,值得單獨記一筆,因為它跟直覺相反:刪檔要的是「父目錄」的寫權限,不是檔案本身的。 我第一版把 token 放在一個 root 擁有的目錄底下,檔案給了 0600、擁有者也設對了,結果 rm 還是 Permission denied。而啟動腳本開了 set -e,於是整個容器 exit 1,畫面上看起來像 Session 建不起來,真正的死因是一行清理指令。

修法有兩半,兩半都要:把 token 連同專用目錄一起送進去,並將兩者設成 Session 使用者擁有;若 rm 仍然失敗,就改以清空檔案內容收尾並出聲,而不是讓整場 Session 跟著死掉。清不掉才是真正少一道保障,必須明確警告。

改完之後 CLI 自己會講:終端裡打 /status,「Auth token」那一行寫的是 CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR

網頁終端裡打 /status,Auth token 那一行是 CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR,不是 token 本身

這一天原本要講的是「我沒有做的那些」,結果多了一件今天才做的。

已有多人管理,缺的是完整帳號生命週期

第四條:沒有完整的帳號生命週期管理。

這套工具不是沒有多人管理面。它已有 admin/user 兩種角色、管理員帳號清單、建立帳號、建立時指定 admin,以及管理員重設密碼;管理員也能跨使用者查看、開啟終端與終止 Session。這些都已經是實質的多人管理能力。

沒有做完整的,是既有帳號的生命週期:帳號不能正式停用/復用,角色建立後不能升降,也沒有獨立的管理動作稽核軌跡(Session 本身另有歷史紀錄,那是另一回事)。

這套工具沒有實作 is_active。現階段採用的代償機制,是由管理員重設密碼:讓既有登入狀態失效,並嘗試切斷目前開著的終端。它處理的是互動存取,不是完整的帳號撤銷;執行中的 Session 仍在,CLI token、GitLab PAT 與 per-user proxy 也不會因此消失。

為什麼停在這裡?這一輪的開發主軸,是先把「建立環境、進行審查、保留紀錄、結束 Session」這條功能路徑做完整。多使用者部分先補上讓服務成立所需的身分、ownership 與基本管理;完整帳號生命週期不只多幾顆按鈕,它還要讓停用狀態同時約束登入、已建立的 WebSocket、執行中的 Session、CLI token、GitLab PAT 與 per-user proxy,失敗時還要能重試與對帳。這已經是另一個子系統,所以這一輪沒有把它涵蓋進來。

如果使用情境需要更完整的生命週期管理,可以在這個基礎上繼續加工:加入停用/復用狀態,並讓 Session、各類憑證與 per-user proxy 跟著收斂;事後升降權限與管理操作的稽核也屬於同一層延伸。這是後續可以發展的方向,不是原則上決定不做。

評估中,不等於決定不做:把模型搬進來

第五條跟前四條都不同:不是不做,是還在評估。

Day 20 撞到、Day 23 用兩把尺量出來的是同一道邊:api.anthropic.com 一定要開,不然沒有 agent;只要那條路開著,伺服器那一端替我連出去的東西,我封不到。Day 23 最後收在「最後得到的不是一個全知視角,而是一份有量程的證據」。

那個邊界不是永遠決定不了。把模型搬進地端,可以拿掉「把 code 送往外部模型 API」這條固定路徑;工具呼叫與其他對外流量,仍得沿用前面建立的網路邊界逐一管理。這樣,邊界才有機會回到自己手上。這條路六月那場演講就講過,當時定調是評估中的方案,硬體從一張顯卡起、可以往上疊;到今天它還是評估中,不是原則上不做。說評估中而不說通了,是因為證據比前四條弱:外面走這條路的人有,Ollama 配 Continue,挑一個真的支援 tool calling 的模型,就能在本機跑起一個會呼叫工具的 coding agent(官方整合頁自己也註明,不是每個宣稱支援的模型都真的能用),NVIDIA 也有一份技術文講怎麼自架讓 code 不出網路;但我自己還沒量過任何一段。沒排進這三十天,是因為它動的是模型那一側,前面那些容器、防火牆、側錄一樣都不用改,只是每一道都要對著換進來的模型重新量一次。

反例:能力做了,就得補回結構

前面三條都沒有把那個能力做成預設、可直接使用的產品表面;第四條已有基本的多人管理,但沒有延伸成完整的帳號生命週期;第五條則還在評估要不要搬進來。今天最後想講一條反方向的,因為它讓前面那個判準有了對照。

昨天說打算補的那條路,做了。PTY 是字元流,圖片這種二進位資料在那條路上沒有通道,所以貼圖得另外開一條:檔案上傳到那一場的持久化目錄,回傳容器內的路徑,人自己把路徑貼進終端讓 AI 去讀。

這條路後來做了:可以在終端裡直接 ⌘V 圖片,或按迴紋針選檔;上傳完成後把容器內路徑放進剪貼簿,剪貼簿不可用時就直接顯示路徑。操作流程不是今天的重點,真正的代價是後面那些必須補回來的控制。

而做的過程裡最有感的一件事是:自己立的閘一旦拆掉,要補回來的比拆掉的多。

原本後端有一條「寫入類請求一律只收 JSON」的規則,NGINX 那邊也有請求大小上限。這兩道一拆,就在控制平面新增了唯一一條檔案上傳路徑:使用者可以把檔案寫進 Session 的持久化目錄。於是補回來的有這些:

  • 上傳請求必須帶自訂標頭。 一般 <form>no-cors 的跨來源請求送不出這個標頭;要送出就必須經過正常的 CORS 流程,因而會先遇到 preflight。這擋的是這一類 CSRF,不是驗明請求一定來自我的前端。SameSite=Lax 是 site 級且不分 port;同一 site 的其他來源,例如 localhost 的另一個 port,仍可能帶上 cookie
  • 副檔名白名單,不是黑名單。 白名單漏掉的是功能,黑名單漏掉的是防線
  • 大小上限三道同向:逐檔驗、框架整包上限、NGINX 那道。而 NGINX 那道要略大於自己那道,否則使用者撞到的是 NGINX 的 413 HTML 頁,不是一句講得清楚的訊息
  • 路徑穿越擋三層,而且三層各自獨立成立:檔名只取 basename、字元白名單化(../、絕對路徑、控制字元、shell 特殊字元一律換成底線),寫入時再以受信任的上傳目錄為錨,逐層拒絕 symlink,不再重新拼接完整路徑

最後那一條的「各自獨立成立」是刻意的。三層不是為了擋三種攻擊,是為了任何一層被後人改壞,另外兩層還在。

而這一條之所以放在今天,是因為它跟前面那四條「不把完整能力做成產品表面」的做法共用同一個判準,只是站在另一邊:當你決定讓一個能力存在,你就得為它付出結構。不能只是「我會小心」。那四條因此少背一部分持續控制的成本;這一條是讓能力存在,所以那四道閘一道都不能少。

講完我這一條,補一個剛好撞上的外部對照。OpenAI 今天(台灣時間 9 月 4 日)發布 GPT-6 Astra,系統卡寫明它是 OpenAI 第一個在 Preparedness Framework 的網路安全項目達到 Critical 等級的模型。我注意的不只是能力本身,還有同一份文件裡,能力上去之後一起補上的東西。內部開發與部署加了更嚴的隔離、checkpoint 加密與更嚴的存取控制,內部使用前還多了一道過不了就不能用的對齊評估。對外部署則替會用工具的推論接上監控,看模型有沒有越出使用者交代的範圍;偵測到可能屬高嚴重度的狀況時,可以自動暫停或結束那場對話。這裡不是要評論它做得好不好,尺度也跟我這套工具差了好幾個數量級,但問題的形狀是同一種:能力做得到是一回事,決定讓它存在之後,要替它補回多少結構,是另一回事。

那,我要背幾條?

寫到這裡我自己回頭看,突然覺得不太對勁。

地端那條還在評估,不算規矩,先放一邊。四條規矩,聽起來像是四個我得一直記在心上、一不小心就會違反的承諾。但真的是這樣嗎?我怎麼可能背四條?

問題出在「背」這個字。能夠被背叛的,只有存在的東西。

那四條裡,有三個缺口能直接從結構查證:路由表上沒有任何一條把文字送進 session 的路徑;資料模型(server/models.py)上沒有任何一個欄位存使用者的私鑰;User 也沒有帳號狀態,路由表上找不到停用、復用或事後升降權限的端點。第一條仍須講清楚:互動終端本身可寫,所以不是「軟體絕對送不進去」;只是要自動送入,得另外理解並操作 ttyd 的 WebSocket 或瀏覽器,而不是呼叫產品已經替你準備好的端點。要讓這三件事正式成為產品能力,都得有人動手新增一整個表面,那不是「一不小心」做得到的事。

真正需要我持續看住的,其實只有一條:啟動那條路徑。因為那裡本來就是軟體在組指令列,要在裡面多塞一個字串,技術上一點都不困難。那才是唯一一個「自律」還有作用的地方,而我對這件事的辦法,是把那句話寫得夠精確,精確到日後有人想加東西進去的時候,會先撞到這句話:

目前的控制平面不會替使用者產生或送出 prompt。

還有一條也在同一個表面上。拿今天那個問法回頭問自己:它說不會做 X 的時候,有沒有做 X 的能力。Day 9 那條「發佈之前要有人點頭」,到今天還是一句寫在 skill 裡的要求。Day 9 當時就寫了,代理管的是端點不是點頭;Day 17 起,容器裡的 CLI 預設就是 claude --dangerously-skip-permissions,連工具權限那一關也不問。代理白名單放行那個 POST,CLI 不攔,攔的只剩 skill 裡那行字。照今天的說法,能力在,承諾靠人記得,那句話的壽命就等於我還記得多久。我沒有把它做成機制,理由跟那四條都不同:不是這個能力不該存在,是我還沒有足夠的觀測,敢讓發報告這一步從人逐次點頭,放到規則自己擋、擋不住才找人。這一格留著人,跟 Day 24 的 rootless 同一種狀態:排在待辦裡的,不是評估完決定不做的。

所以答案是:前面四條裡,我只需要背一條;再加上 Day 9 的發布確認,目前共有兩條仍由人看守,另外三條由結構替我背著。

五條規矩,只有兩條需要人記得:三條由結構背著,兩條由人背著

而這正好是我一直想要的那個形狀。前面幾天講權限、講邊界、講對帳的時候,重複出現的都是同一句:我要它是結構性的,不是靠自律。 而今天這一整篇其實是那句話最完整的一次落地,因為它讓我看清楚一件事:所謂「決定不做」,如果只停在決定,那還是自律;要讓它變成結構,就得讓那個能力不成為預設、可直接使用的產品表面,而且讓別人能夠查證。

本日小結

把這些項目放回同一張表,差別會比一串「做了/沒做」更清楚:

能力 技術上能不能做 現在是不是產品能力 為什麼停在這裡 剩餘風險或代價
控制平面替人產生或送出 prompt 條款邊界讀起來是灰的,而這項能力沒有重要到值得靠近那片灰區 ttyd 終端本身仍可寫;有存取權的人若另外操作 WebSocket 或瀏覽器,仍能自動化
專用的程式化送入 API 不讓 /input/keys 成為預設、可直接呼叫的產品表面 沒有端點不等於底層無法送字,只是不能沿著產品準備好的路徑直接呼叫
代管使用者的 SSH 私鑰 不承接一把可能通往多台主機、又得逐台撤銷的長期秘密;GitLab 改走可到期、可集中撤銷的 PAT 代理 部署者仍可開啟共用 SSH agent;若沒有 destination constraints,能力可能延伸到其他主機
完整帳號生命週期 只有基本多人管理 停用/復用、事後升降權限與管理稽核會同時牽動登入、WebSocket、Session、憑證與 per-user proxy,是另一個子系統 目前重設密碼只能收回互動存取,執行中的 Session、CLI token、GitLab PAT 與 proxy 不會跟著消失
地端模型 外部已有路徑,我還沒量過 否,仍在評估 它能拿掉 code 固定送往外部模型 API 的路徑,但換模型後,每一道治理措施都得重新量 工具呼叫與其他對外流量仍要逐一管理,也還沒有自己的效能與品質數據
Claude Code CLI 登入憑證 非拿不可 是,改由 anonymous pipe 的 file descriptor 送入 CLI 必須取得自己的身份;能做的是移除環境變數、docker inspect 與啟動前程序這些意外暴露面 這不代表同 uid 的 agent 絕對碰不到憑證,只是拿掉最省事的外流路徑
圖片上傳 PTY 只有字元流,二進位資料需要另一條通道 能力一旦存在,就得持續支付 CSRF、白名單、大小上限與路徑穿越防護的成本

真正要分的不是「有做」和「沒做」兩類,而是三種狀態:有些能力從產品表面拿掉了;有些非拿不可,只能縮小暴露面;另一些仍待完成,不能包裝成原則上不需要。若只算前面四條設計界線,再加上 Day 9 的發布確認,目前共有五條:三條由結構背著,控制平面的啟動界線與發布確認仍由人看守。地端模型仍在評估;CLI 憑證與圖片上傳,則是能力存在之後如何承擔代價的另一類問題。

決定不做這件事最難的地方,是它通常沒有一張成品截圖可以展示。你能給出來的證據,是一份清單,說明這裡本來可以有什麼、以及為什麼它現在沒有被做成產品表面。今天這篇就是那份清單。

明天講召喚出來的東西誰收。


上一篇
Day 26|AI 用 Rust 重寫 ttyd:怎麼證明它沒改壞?
下一篇
Day 28|收得掉,也長得回來才算平台:對帳迴圈與 40 分鐘全站卡死
系列文
AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言